package org.bronycorps.mlptss.manager;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.bronycorps.mlptss.domain.actor.BossActor;
import org.bronycorps.mlptss.domain.actor.BulletActor;
import org.bronycorps.mlptss.domain.actor.EnemyActor;
import org.bronycorps.mlptss.domain.actor.HighlightActor;
import org.bronycorps.mlptss.domain.actor.MissileActor;
import org.bronycorps.mlptss.domain.actor.PlayerActor;
import org.bronycorps.mlptss.domain.actor.SpecialMissileActor;
import org.bronycorps.mlptss.domain.ai.actor.VirtualDestructibleActor;
import org.bronycorps.mlptss.domain.ai.actor.VirtualIndestructibleActor;
import org.bronycorps.mlptss.domain.ai.tweenable.VirtualDestructibleAccessor;
import org.bronycorps.mlptss.domain.ai.tweenable.VirtualIndestructibleAccessor;
import org.bronycorps.mlptss.domain.model.Boss;
import org.bronycorps.mlptss.domain.model.Enemy;
import org.bronycorps.mlptss.domain.model.Missile.MissileTraject;
import org.bronycorps.mlptss.domain.model.Player;
import org.bronycorps.mlptss.domain.model.Player.State;
import org.bronycorps.mlptss.domain.tweenable.BossAccessor;
import org.bronycorps.mlptss.domain.tweenable.BulletAccessor;
import org.bronycorps.mlptss.domain.tweenable.EnemyAccessor;
import org.bronycorps.mlptss.domain.tweenable.HighlightActorAccessor;
import org.bronycorps.mlptss.domain.tweenable.MissileAccessor;
import org.bronycorps.mlptss.domain.tweenable.PlayerAccessor;
import org.bronycorps.mlptss.domain.tweenable.SpecialMissileAccessor;
import org.bronycorps.mlptss.manager.preferences.TrajectConfiguration;
import org.bronycorps.mlptss.manager.preferences.TrajectConfiguration.DelayTweenConfig;
import org.bronycorps.mlptss.manager.preferences.TrajectConfiguration.InitialSetConfig;
import org.bronycorps.mlptss.manager.preferences.TrajectConfiguration.ScaleTweenConfig;
import org.bronycorps.mlptss.manager.preferences.TrajectConfiguration.TrajectTweenConfig;
import org.bronycorps.mlptss.screen.LevelScreen;
import org.bronycorps.mlptss.utils.VectorUtils;

import aurelienribon.tweenengine.BaseTween;
import aurelienribon.tweenengine.Timeline;
import aurelienribon.tweenengine.Tween;
import aurelienribon.tweenengine.TweenCallback;
import aurelienribon.tweenengine.TweenEquation;
import aurelienribon.tweenengine.TweenManager;
import aurelienribon.tweenengine.equations.Back;
import aurelienribon.tweenengine.equations.Bounce;
import aurelienribon.tweenengine.equations.Circ;
import aurelienribon.tweenengine.equations.Cubic;
import aurelienribon.tweenengine.equations.Elastic;
import aurelienribon.tweenengine.equations.Expo;
import aurelienribon.tweenengine.equations.Linear;
import aurelienribon.tweenengine.equations.Quad;
import aurelienribon.tweenengine.equations.Quart;
import aurelienribon.tweenengine.equations.Quint;
import aurelienribon.tweenengine.equations.Sine;

import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.utils.OrderedMap;

public class TrajectFactory {

	public enum BulleTTrajectType {
		SIMPLE
	}

	public enum MissileTrajectType {
		STRAIGHT, VERTICAL
	}

	private static Map<String, TweenEquation> equationMap = new HashMap<String, TweenEquation>() {
		{
			put("linear", Linear.INOUT);
			put("quad", Quad.INOUT);
			put("quad.in", Quad.IN);
			put("quad.out", Quad.OUT);
			put("back", Back.INOUT);
			put("bounce", Bounce.INOUT);
			put("circ", Circ.INOUT);
			put("cubic", Cubic.INOUT);
			put("elastic", Elastic.INOUT);
			put("expo", Expo.INOUT);
			put("quart", Quart.INOUT);
			put("quint", Quint.INOUT);
			put("sine", Sine.INOUT);
		}
	};

	private static TrajectFactory instance = null;

	final public static TrajectFactory getInstance() {
		if (instance == null) {
			instance = new TrajectFactory();
		}
		return instance;
	}

	private TweenManager tweenManager;

	private TrajectFactory() {
		super();
		this.tweenManager = new TweenManager();
		Tween.registerAccessor(EnemyActor.class, new EnemyAccessor());
		Tween.registerAccessor(BulletActor.class, new BulletAccessor());
		Tween.registerAccessor(HighlightActor.class,
				new HighlightActorAccessor());
		Tween.registerAccessor(MissileActor.class, new MissileAccessor());
		Tween.registerAccessor(BossActor.class, new BossAccessor());
		Tween.registerAccessor(SpecialMissileActor.class,
				new SpecialMissileAccessor());
		Tween.registerAccessor(Player.class, new PlayerAccessor());
		Tween.setWaypointsLimit(10);
	}

	public void clearTweens(Object target) {
		this.tweenManager.killTarget(target);
	}

	private Timeline createMissileTween(MissileActor missile,
			MissileTraject traject) {
		Timeline result = Timeline.createSequence().beginSequence();

		switch (traject.getTrajectType()) {
		case STRAIGHT:
			result.beginSequence().push(
					Tween.to(missile, MissileAccessor.POSITION_XY, 2)
							.waypoint(traject.getWaypoint().x,
									traject.getWaypoint().y)
							.target(traject.getDestinationPoint().x,
									traject.getDestinationPoint().y)
							.ease(Linear.INOUT));
			break;
		case VERTICAL:
			result.beginSequence()
					.push(Tween
							.to(missile, MissileAccessor.POSITION_XY, 1)
							.target(traject.getWaypoint().x,
									traject.getWaypoint().y).ease(Linear.INOUT))
					.push(Tween
							.to(missile, MissileAccessor.POSITION_XY, 1)
							.target(traject.getDestinationPoint().x,
									traject.getDestinationPoint().y)
							.ease(Linear.INOUT)).end();
			break;
		}

		return result;
	}

	public void generateBossTraject(BossActor boss) {

		final float deltaAngle = 360f / 5f * 2;

		List<Vector2> waypoints = new ArrayList<Vector2>();

		float bossHeight = boss.getImageHeight();
		float width = boss.getImageWidth();

		float space = (LevelScreen.STAGE_HEIGHT / 2 - bossHeight) / 4;

		final Vector2 center = new Vector2(LevelScreen.STAGE_WIDTH - width
				- space * 2, LevelScreen.STAGE_HEIGHT / 4);

		Vector2 point = new Vector2(space, 0);

		// for the actor that looks like floating around, we make it floating a
		// few times in a traject
		for (int pass = 0; pass < 1; pass++) {
			// set original angle as random, so it looks more natural
			point.setAngle((float) (Math.random() * 360));
			// set waypoints around the center like a pentagram
			float direction = Math.random() > 0.5 ? 1 : -1;
			for (int step = 0; step < 5; step++) {
				point.setAngle(point.angle() + direction * deltaAngle);
				waypoints.add(point.cpy().add(center));
			}
		}

		((Boss) boss.getModel()).setWaypoints(waypoints);
	}

	public void generateMissileTraject(final MissileActor missile,
			Vector2 initialPosition, PlayerActor target,
			MissileTrajectType trajetType) {

		Vector2 targetPosition = null, waypoint = null;

		switch (trajetType) {
		case STRAIGHT:
			targetPosition = new Vector2(0, (float) Math.random()
					* LevelScreen.STAGE_HEIGHT / 2 + target.getTrack().getY());

			waypoint = new Vector2(
					(float) (Math.random() * (LevelScreen.STAGE_WIDTH / 2) + LevelScreen.STAGE_WIDTH / 2),
					LevelScreen.STAGE_HEIGHT / 2);

			break;
		case VERTICAL:
			targetPosition = new Vector2(0, target.getModelPosition().y
					+ target.getTrack().getY()).sub(20, 0).add(0,
					(float) Math.random() * 100 - 50);

			waypoint = targetPosition.cpy()
					.add(LevelScreen.STAGE_WIDTH - 50, 0);
			break;
		}

		missile.getMissile()
				.getModel()
				.setTraject(
						new MissileTraject(MissileTrajectType.STRAIGHT,
								initialPosition, waypoint, targetPosition));

	}

	public TweenManager getTweenManager() {
		return this.tweenManager;
	}

	public void tweenBoss(final BossActor bossActor) {

		List<Vector2> waypoints = ((Boss) bossActor.getModel()).getWaypoints();
		float speed = 3f;

		final float durationStep1 = 2f;
		final float durationStep2 = waypoints.size() * speed;
		final float durationStep3 = 3f;

		Timeline timeline = Timeline.createSequence().beginSequence();
		// step 1 : the boss is coming
		timeline.push(
				Tween.set(bossActor, BossAccessor.POSITION_XY).target(
						LevelScreen.STAGE_WIDTH + 200,
						LevelScreen.STAGE_HEIGHT / 4)).push(
				Tween.to(bossActor, BossAccessor.POSITION_XY, durationStep1)
						.target(waypoints.get(0).x,
								LevelScreen.STAGE_HEIGHT / 4));

		// step 2 : idling and floating around

		Tween idleTraject = Tween
				.to(bossActor, BossAccessor.POSITION_XY, durationStep2)
				.target(waypoints.get(0).x, LevelScreen.STAGE_HEIGHT / 4)
				.ease(Linear.INOUT);
		for (Vector2 wp : waypoints) {
			idleTraject.waypoint(wp.x, wp.y);
		}
		timeline.push(idleTraject);

		// step 3 : going away
		timeline.push(Tween.to(bossActor, BossAccessor.POSITION_XY,
				durationStep3).target(-200, LevelScreen.STAGE_HEIGHT / 4));
		timeline.setCallback(new TweenCallback() {

			@Override
			public void onEvent(int type, BaseTween<?> source) {
				bossActor.terminate();
			}
		}).start(this.tweenManager);

		// setting special missile attack at step 2
		Tween.call(new TweenCallback() {

			@Override
			public void onEvent(int type, BaseTween<?> source) {
				((Boss) bossActor.getModel()).getOwner()
						.fireSpecialMissileEvent();
			}
		}).delay(durationStep1 + durationStep2 / 2).start(this.tweenManager);
	}

	public void tweenBullet(final BulletActor bullet,
			BulleTTrajectType trajectType) {
		switch (trajectType) {
		case SIMPLE:
			Tween.to(bullet, BulletAccessor.POSITION_X,
					bullet.getBullet().getModel().getType().getLifeDuration())
					.target(bullet.getX() + LevelScreen.STAGE_WIDTH)
					.ease(Linear.INOUT).setCallback(new TweenCallback() {
						@Override
						public void onEvent(int type, BaseTween<?> source) {
							bullet.terminate();
						}
					}).start(this.tweenManager);
			break;
		default:
			break;
		}
	}

	public void tweenEnemy(final EnemyActor enemy,
			TrajectConfiguration trajectConfig) {

		Timeline sequence = Timeline.createSequence().beginSequence();

		// parse config
		for (Object node : trajectConfig.getNodes()) {
			if (node instanceof InitialSetConfig) {
				InitialSetConfig cfg = (InitialSetConfig) node;
				sequence.push(
						Tween.set(enemy, EnemyAccessor.SCALE).target(
								cfg.getScale()))
						.pushPause(cfg.getDelay())
						.push(Tween.set(enemy, EnemyAccessor.POSITION_XY)
								.target(cfg.getPosition().x,
										cfg.getPosition().y));
			} else if (node instanceof DelayTweenConfig) {
				// Delay
				sequence.pushPause(((DelayTweenConfig) node).getDelay());
			} else if (node instanceof ScaleTweenConfig) {
				// Scaling
				ScaleTweenConfig scaleConfig = (ScaleTweenConfig) node;

				if ("set".equals(scaleConfig.getEase())) {
					sequence.push(Tween.set(enemy, EnemyAccessor.SCALE).target(
							scaleConfig.getTarget()));
				} else {

					Tween tween = Tween.to(enemy, EnemyAccessor.SCALE,
							scaleConfig.getDuration()).target(
							scaleConfig.getTarget());
					TweenEquation equation = equationMap.get(scaleConfig
							.getEase());
					if (equation != null) {
						tween.ease(equation);
					} else {
						tween.ease(Linear.INOUT);
					}
					sequence.push(tween);
				}

			} else if (node instanceof TrajectTweenConfig) {
				// Moving
				TrajectTweenConfig config = (TrajectTweenConfig) node;

				if ("set".equals(config.getEase())) {
					sequence.push(Tween.set(enemy, EnemyAccessor.POSITION_XY)
							.target(config.getTarget().x, config.getTarget().y));
				} else {
					Tween tween = Tween.to(enemy, EnemyAccessor.POSITION_XY,
							config.getDuration()).target(config.getTarget().x,
							config.getTarget().y);
					TweenEquation equation = equationMap.get(config.getEase());
					if (equation != null) {
						tween.ease(equation);
					} else {
						tween.ease(Linear.INOUT);
					}

					for (Object waypoint : config.getWaypoints()) {
						OrderedMap<String, Float> wp = (OrderedMap<String, Float>) waypoint;
						tween.waypoint(wp.get("x"), wp.get("y"));
					}
					sequence.push(tween);
				}
			}
		}

		// add callback and start it

		sequence.setCallback(new TweenCallback() {
			@Override
			public void onEvent(int type, BaseTween<?> source) {
				enemy.terminate();
			}
		}).end().start(this.tweenManager);

	}

	public void tweenHittedPlayer(final Player player) {
		this.tweenManager.killTarget(player);

		Tween.call(new TweenCallback() {

			@Override
			public void onEvent(int type, BaseTween<?> source) {
				player.setState(Player.State.NORMAL);
			}
		}).delay(5).start(this.tweenManager);
	}

	public void tweenKnockBackPlayer(final Player player, Enemy enemy) {
		this.tweenManager.killTarget(player);

		Vector2 finalPosition = VectorUtils.calculateKnockBack(
				player.getPosition(), enemy.getPosition(), 150f);

		Tween.to(player, PlayerAccessor.POSITION_XY, 0.5f)
				.targetRelative(finalPosition.x, finalPosition.y)
				.start(this.tweenManager);

		Tween.call(new TweenCallback() {
			@Override
			public void onEvent(int type, BaseTween<?> source) {
				player.setState(State.WEAK);
			}
		}).delay(2).start(this.tweenManager);

		Tween.call(new TweenCallback() {
			@Override
			public void onEvent(int type, BaseTween<?> source) {
				player.setState(State.NORMAL);
			}
		}).delay(10).start(this.tweenManager);
	}

	public void tweenMissile(final MissileActor missile) {

		MissileTraject traject = missile.getMissile().getModel().getTraject();

		TweenCallback callback = new TweenCallback() {
			@Override
			public void onEvent(int type, BaseTween<?> source) {
				missile.terminate();
			}
		};

		createMissileTween(missile, traject).setCallback(callback).start(
				this.tweenManager);

	}

	public Timeline tweenVirtualEnemy(VirtualDestructibleActor enemy,
			TrajectConfiguration trajectConfig) {
		Timeline sequence = Timeline.createSequence().beginSequence();

		// parse config
		for (Object node : trajectConfig.getNodes()) {
			if (node instanceof InitialSetConfig) {
				InitialSetConfig cfg = (InitialSetConfig) node;
				sequence.push(
						Tween.set(enemy, VirtualDestructibleAccessor.SCALE)
								.target(cfg.getScale()))
						.pushPause(cfg.getDelay())
						.push(Tween.set(enemy,
								VirtualDestructibleAccessor.POSITION_XY)
								.target(cfg.getPosition().x,
										cfg.getPosition().y));
			} else if (node instanceof DelayTweenConfig) {
				// Delay
				sequence.pushPause(((DelayTweenConfig) node).getDelay());
			} else if (node instanceof ScaleTweenConfig) {
				// Scaling
				ScaleTweenConfig scaleConfig = (ScaleTweenConfig) node;

				if ("set".equals(scaleConfig.getEase())) {
					sequence.push(Tween.set(enemy,
							VirtualDestructibleAccessor.SCALE).target(
							scaleConfig.getTarget()));
				} else {

					Tween tween = Tween.to(enemy,
							VirtualDestructibleAccessor.SCALE,
							scaleConfig.getDuration()).target(
							scaleConfig.getTarget());
					TweenEquation equation = equationMap.get(scaleConfig
							.getEase());
					if (equation != null) {
						tween.ease(equation);
					} else {
						tween.ease(Linear.INOUT);
					}
					sequence.push(tween);
				}

			} else if (node instanceof TrajectTweenConfig) {
				// Moving
				TrajectTweenConfig config = (TrajectTweenConfig) node;

				if ("set".equals(config.getEase())) {
					sequence.push(Tween.set(enemy,
							VirtualDestructibleAccessor.POSITION_XY).target(
							config.getTarget().x, config.getTarget().y));
				} else {
					Tween tween = Tween.to(enemy,
							VirtualDestructibleAccessor.POSITION_XY,
							config.getDuration()).target(config.getTarget().x,
							config.getTarget().y);
					TweenEquation equation = equationMap.get(config.getEase());
					if (equation != null) {
						tween.ease(equation);
					} else {
						tween.ease(Linear.INOUT);
					}

					for (Object waypoint : config.getWaypoints()) {
						OrderedMap<String, Float> wp = (OrderedMap<String, Float>) waypoint;
						tween.waypoint(wp.get("x"), wp.get("y"));
					}
					sequence.push(tween);
				}
			}
		}

		return sequence;
	}

	public Timeline tweenVirtualMissile(VirtualIndestructibleActor missile,
			MissileTraject traject) {

		float offset = missile.getSource().getTrack().getY();

		Timeline result = Timeline.createSequence().beginSequence();

		switch (traject.getTrajectType()) {
		case STRAIGHT:
			result.push(Tween
					.to(missile, VirtualIndestructibleAccessor.POSITION_XY, 2)
					.waypoint(traject.getWaypoint().x,
							traject.getWaypoint().y - offset)
					.target(traject.getDestinationPoint().x,
							traject.getDestinationPoint().y - offset)
					.ease(Linear.INOUT));
			break;
		case VERTICAL:
			result.push(
					Tween.to(missile,
							VirtualIndestructibleAccessor.POSITION_XY, 1)
							.target(traject.getWaypoint().x,
									traject.getWaypoint().y - offset)
							.ease(Linear.INOUT))
					.push(Tween
							.to(missile,
									VirtualIndestructibleAccessor.POSITION_XY,
									1)
							.target(traject.getDestinationPoint().x,
									traject.getDestinationPoint().y - offset)
							.ease(Linear.INOUT)).end();
			break;
		}

		return result;

	}

}
